﻿#region Usings

using System;
using System.Linq;
using System.Reflection;

#endregion

namespace Yandex.Market.Api.YmlSerialization
{
    /// <summary>
    ///   A wrapper class for members which only can be properties or member variables
    /// </summary>
    internal class MemberWrapper
    {
        #region Fields 

        /// <summary>
        ///   the <c>FieldInfo</c> instance, if this member corrsponds to a field, <c>null</c> otherwise
        /// </summary>
        private readonly FieldInfo _fieldInfoInstance;

        /// <summary>
        ///   <c>true</c> if this instance corresponds to a property, <c>false</c> 
        ///   if it corrsponds to a field (i.e., a member variable)
        /// </summary>
        private readonly bool _isProperty;

        /// <summary>
        ///   reference to the underlying <c>MemberInfo</c> from which this instance is built
        /// </summary>
        private readonly MemberInfo _memberInfo;

        /// <summary>
        ///   the member type of the underlying member
        /// </summary>
        private readonly Type _memberType;

        /// <summary>
        ///   a type wrapper around the underlying member type
        /// </summary>
        private readonly UdtWrapper _memberTypeWrapper;

        /// <summary>
        ///   the <c>PropertyInfo</c> instance, if this member corrsponds to a property, <c>null</c> otherwise
        /// </summary>
        private readonly PropertyInfo _propertyInfoInstance;

        /// <summary>
        ///   The alias specified by the user
        /// </summary>
        private string _alias = "";

        /// <summary>
        ///   The collection attribute instance
        /// </summary>
        private YmlCollectionAttribute _collectionAttributeInstance;

        /// <summary>
        ///   specifies whether this member is going to be serialized as an attribute
        /// </summary>
        private bool _isSerializedAsAttribute;

        /// <summary>
        ///   specifies whether this member is going to be serialized as a value for another element
        /// </summary>
        private bool _isSerializedAsValue;

        /// <summary>
        ///   The location of the serialization
        /// </summary>
        private string _serializationLocation = "";

        #endregion Fields

        #region Constructors 

        /// <summary>
        ///   Initializes a new instance of the <see cref = "MemberWrapper" /> class.
        /// </summary>
        /// <param name = "memberInfo">The member-info to build this instance from.</param>
        /// <param name = "callerSerializer">The caller serializer.</param>
        public MemberWrapper(MemberInfo memberInfo, YmlSerializer callerSerializer)
        {
            if(!(memberInfo.MemberType == MemberTypes.Property || memberInfo.MemberType == MemberTypes.Field))
                throw new Exception("Member must be either property or field");

            _memberInfo = memberInfo;
            _isProperty = (memberInfo.MemberType == MemberTypes.Property);

            Alias = _memberInfo.Name;

            if(_isProperty)
                _propertyInfoInstance = (PropertyInfo)memberInfo;
            else
                _fieldInfoInstance = (FieldInfo)memberInfo;

            _memberType = _isProperty ? _propertyInfoInstance.PropertyType : _fieldInfoInstance.FieldType;

            _memberTypeWrapper = TypeWrappersPool.Pool.GetTypeWrapper(MemberType, callerSerializer);

            InitInstance();

            TreatErrorsAs = callerSerializer != null ? callerSerializer.DefaultExceptionType : YmlExceptionTypes.Error;

            var attrs = _memberInfo.GetCustomAttributes(false);
            foreach(var attr in attrs.OfType<YmlBaseAttribute>())
                ProcessYmlAttribute(attr);

            foreach(var attr in attrs.OfType<YmlBaseAttribute>())
            {
                if(IsSerializedAsElement && attr is YmlElementOrderAttribute)
                {
                    YmlElementOrderAttribute orderAttribute = attr as YmlElementOrderAttribute;
                    Order = orderAttribute.Order;
                }
                else if(IsSerializedAsAttribute && attr is YmlAttributeOrderAttribute)
                {
                    YmlAttributeOrderAttribute attributeOrderAttribute = attr as YmlAttributeOrderAttribute;
                    AttributeOrder = attributeOrderAttribute.Order;
                }
            }
        }

        #endregion Constructors 

        #region Properties 

        /// <summary>
        ///   Gets the alias specified for this member.
        /// </summary>
        /// <value>The alias specified for this member.</value>
        public string Alias
        {
            get { return _alias; }

            private set { _alias = StringUtils.RefineSingleElement(value); }
        }

        /// <summary>
        ///   Gets a value indicating whether the member corrsponding to this instance can be read from.
        /// </summary>
        /// <value><c>true</c> if the member corrsponding to this instance can be read from; otherwise, <c>false</c>.</value>
        public bool CanRead
        {
            get { return !_isProperty || _propertyInfoInstance.CanRead; }
        }

        /// <summary>
        ///   Gets a value indicating whether the member corrsponding to this instance can be written to.
        /// </summary>
        /// <value><c>true</c> if the member corrsponding to this instance can be written to; otherwise, <c>false</c>.</value>
        public bool CanWrite
        {
            get { return !_isProperty || _propertyInfoInstance.CanWrite; }
        }

        /// <summary>
        ///   Gets an array of comment lines.
        /// </summary>
        /// <value>The comment lines.</value>
        public string[] Comment { get; private set; }

        /// <summary>
        ///   Gets the default value for this instance.
        /// </summary>
        /// <value>The default value for this instance.</value>
        public object DefaultValue { get; private set; }

        /// <summary>
        ///   Gets the format specified for this value; <c>null</c> if no format is specified.
        /// </summary>
        /// <value>the format specified for this value; <c>null</c> if no format is specified.</value>
        public string Format { get; private set; }

        /// <summary>
        ///   Gets a value indicating whether this instance has comments.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance has comments; otherwise, <c>false</c>.
        /// </value>
        public bool HasComment
        {
            get { return Comment != null && Comment.Length > 0; }
        }

        /// <summary>
        ///   Gets a value indicating whether this instance has format values specified
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance has format values specified; otherwise, <c>false</c>.
        /// </value>
        public bool HasFormat
        {
            get
            {
                // empty string may be considered as a valid format
                return Format != null;
            }
        }

        /// <summary>
        ///   Gets a value indicating whether this instance is attributed as dont serialize.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is attributed as dont serialize; otherwise, <c>false</c>.
        /// </value>
        public bool IsAttributedAsDontSerialize { get; private set; }

        /// <summary>
        ///   Gets a value indicating whether this instance is attributed as not-collection.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is attributed as not-collection; otherwise, <c>false</c>.
        /// </value>
        public bool IsAttributedAsNotCollection { get; private set; }

        /// <summary>
        ///   Gets a value indicating whether this instance is attributed as serializable.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is attributed as serializable; otherwise, <c>false</c>.
        /// </value>
        public bool IsAttributedAsSerializable { get; private set; }

        /// <summary>
        ///   Gets a value indicating whether this instance is serialized as an XML attribute.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is serialized as an XML attribute; otherwise, <c>false</c>.
        /// </value>
        public bool IsSerializedAsAttribute
        {
            get { return _isSerializedAsAttribute; }

            private set
            {
                _isSerializedAsAttribute = value;
                if(value)
                {
                    // a field cannot be both serialized as an attribute and a value
                    _isSerializedAsValue = false;
                }
            }
        }

        /// <summary>
        ///   Gets a value indicating whether this instance is serialized as a value for an element.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is serialized as a value for an element; otherwise, <c>false</c>.
        /// </value>
        public bool IsSerializedAsValue
        {
            get { return _isSerializedAsValue; }

            private set
            {
                _isSerializedAsValue = value;
                if(value)
                {
                    // a field cannot be both serialized as an attribute and a value
                    _isSerializedAsAttribute = false;
                }
            }
        }

        /// <summary>
        ///   Gets a value indicating whether this instance is serialized as an XML element.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is serialized as an XML element; otherwise, <c>false</c>.
        /// </value>
        public bool IsSerializedAsElement
        {
            get { return !IsSerializedAsAttribute && !IsSerializedAsValue; }

            private set
            {
                if(value)
                {
                    _isSerializedAsAttribute = false;
                    _isSerializedAsValue = false;
                }
            }
        }

        /// <summary>
        ///   Gets the type of the member.
        /// </summary>
        /// <value>The type of the member.</value>
        public Type MemberType
        {
            get { return _memberType; }
        }

        /// <summary>
        ///   Gets the type wrapper instance corrsponding to the member-type of this instance.
        /// </summary>
        /// <value>The type wrapper instance corrsponding to the member-type of this instance.</value>
        public UdtWrapper MemberTypeWrapper
        {
            get { return _memberTypeWrapper; }
        }

        /// <summary>
        ///   Gets the original of this member (as opposed to its alias).
        /// </summary>
        /// <value>The original of this member .</value>
        public string OriginalName
        {
            get { return _memberInfo.Name; }
        }

        /// <summary>
        ///   Gets the serialization location.
        /// </summary>
        /// <value>The serialization location.</value>
        public string SerializationLocation
        {
            get { return _serializationLocation; }

            private set { _serializationLocation = StringUtils.RefineLocationString(value); }
        }

        /// <summary>
        ///   Gets the exception type for this instance in case of encountering missing values
        /// </summary>
        /// <value>The exception type for this instance in case of encountering missing values</value>
        public YmlExceptionTypes TreatErrorsAs { get; private set; }

        /// <summary>
        ///   Gets the collection attribute instance.
        /// </summary>
        /// <value>The collection attribute instance.</value>
        public YmlCollectionAttribute CollectionAttributeInstance
        {
            get { return _collectionAttributeInstance; }
        }

        /// <summary>
        ///   Gets a value indicating whether this instance is treated as a collection.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is treated as a collection; otherwise, <c>false</c>.
        /// </value>
        public bool IsTreatedAsCollection
        {
            get { return !IsAttributedAsNotCollection && _memberTypeWrapper.IsTreatedAsCollection; }
        }

        /// <summary>
        ///   Gets or sets the type of the custom serializer.
        /// </summary>
        /// <value>The type of the custom serializer.</value>
        public Type CustomSerializerType { get; private set; }

        /// <summary>
        ///   Gets a value indicating whether this instance has custom serializer.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance has custom serializer; otherwise, <c>false</c>.
        /// </value>
        public bool HasCustomSerializer
        {
            get { return CustomSerializerType != null; }
        }

        public uint Order { get; private set; }

        public uint AttributeOrder { get; private set; }

        #endregion Properties 

        #region Methods 

        // Public Methods

        /// <summary>
        ///   Gets the original value of this member in the specified object
        /// </summary>
        /// <param name = "obj">The object whose value corresponding to this instance, must be retreived.</param>
        /// <param name = "index">The array of indeces (usually <c>null</c>).</param>
        /// <returns>the original value of this member in the specified object</returns>
        public object GetOriginalValue(object obj, object[] index)
        {
            return _isProperty ? _propertyInfoInstance.GetValue(obj, index) : _fieldInfoInstance.GetValue(obj);
        }

        /// <summary>
        ///   Gets the processed value of this member in the specified object
        /// </summary>
        /// <param name = "obj">The object whose value corresponding to this instance, must be retreived.</param>
        /// <returns>the processed value of this member in the specified object</returns>
        public object GetValue(object obj)
        {
            object elementValue = GetOriginalValue(obj, null);

            if(elementValue == null)
                return null;

            if(elementValue is string && string.IsNullOrEmpty(elementValue as string))
                return null;

            if(_memberTypeWrapper.IsEnum)
                return _memberTypeWrapper.EnumWrapper.GetAlias(elementValue);

            // trying to build the element value
            if(HasFormat && !IsTreatedAsCollection)
            {
                // do the formatting. If formatting succeeds the type of 
                // the elementValue will become 'System.String'
                elementValue = ReflectionUtils.TryFormatObject(elementValue, Format);
            }

            return elementValue;
        }

        /// <summary>
        ///   Sets the value of this member in the specified object
        /// </summary>
        /// <param name = "obj">The object whose member corresponding to this instance, must be given value.</param>
        /// <param name = "value">The value.</param>
        public void SetValue(object obj, object value)
        {
            if(_isProperty)
                _propertyInfoInstance.SetValue(obj, value, null);
            else
                _fieldInfoInstance.SetValue(obj, value);
        }

        /// <summary>
        ///   Determines whether this instance of <c>MemberWrapper</c> can be serialized.
        /// </summary>
        /// <param name = "serializationFields">The serialization fields.</param>
        /// <returns>
        ///   <c>true</c> if this instance of <c>MemberWrapper</c> can be serialized; otherwise, <c>false</c>.
        /// </returns>
        public bool IsAllowedToBeSerialized(YmlSerializationFields serializationFields)
        {
            if(serializationFields == YmlSerializationFields.AllFields)
                return !IsAttributedAsDontSerialize;
            if(serializationFields == YmlSerializationFields.AttributedFieldsOnly)
                return !IsAttributedAsDontSerialize && IsAttributedAsSerializable;
            if(serializationFields == YmlSerializationFields.PublicPropertiesOnly)
                return !IsAttributedAsDontSerialize && _isProperty
                       && ReflectionUtils.IsPublicProperty(_propertyInfoInstance);
            throw new Exception("Unknown serialization field option");
        }

        /// <summary>
        ///   Returns a <see cref = "T:System.String" /> that represents the current <see cref = "T:System.Object" />.
        /// </summary>
        /// <returns>
        ///   A <see cref = "T:System.String" /> that represents the current <see cref = "T:System.Object" />.
        /// </returns>
        public override string ToString()
        {
            return _memberInfo.ToString();
        }

        // Private Methods 

        /// <summary>
        ///   Initializes this instance of <c>MemberWrapper</c>.
        /// </summary>
        private void InitInstance()
        {
            Comment = null;
            IsAttributedAsSerializable = false;
            IsAttributedAsDontSerialize = false;
            IsAttributedAsNotCollection = false;
            IsSerializedAsAttribute = false;
            IsSerializedAsValue = false;
            SerializationLocation = ".";
            Format = null;
            Order = uint.MaxValue;
            AttributeOrder = 0;
            InitDefaultValue();
        }

        /// <summary>
        ///   Initializes the default value for this instance of <c>MemberWrapper</c>.
        /// </summary>
        private void InitDefaultValue()
        {
            DefaultValue = MemberType.IsValueType
                               ? MemberType.InvokeMember(string.Empty,
                                                         BindingFlags.CreateInstance,
                                                         null,
                                                         null,
                                                         new object[0])
                               : null;
        }

        /// <summary>
        ///   Processes the specified attribute which is an instance of <c>YmlAttribute</c>.
        /// </summary>
        /// <param name = "attr">The attribute to process.</param>
        private void ProcessYmlAttribute(object attr)
        {
            if(attr is YmlCommentAttribute)
            {
                string comment = (attr as YmlCommentAttribute).Comment;
                if(!String.IsNullOrEmpty(comment))
                {
                    string[] comments = comment.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
                    for(int i = 0; i < comments.Length; i++)
                        comments[i] = String.Format(" {0} ", comments[i].Trim());

                    Comment = comments;
                }
            }
            else if(attr is YmlSerializableFieldAttribute)
                IsAttributedAsSerializable = true;
            else if(attr is YmlAttributeForClassAttribute)
            {
                if(ReflectionUtils.IsBasicType(MemberType))
                {
                    IsSerializedAsAttribute = true;
                    SerializationLocation = ".";
                }
            }
            else if(attr is YmlAttributeForAttribute)
            {
                if(ReflectionUtils.IsBasicType(MemberType))
                {
                    IsSerializedAsAttribute = true;
                    SerializationLocation = (attr as YmlAttributeForAttribute).Parent;
                }
            }
            else if(attr is YmlValueForClassAttribute)
            {
                if(ReflectionUtils.IsBasicType(MemberType))
                {
                    IsSerializedAsValue = true;
                    SerializationLocation = ".";
                }
            }
            else if(attr is YmlValueForAttribute)
            {
                if(ReflectionUtils.IsBasicType(MemberType))
                {
                    IsSerializedAsValue = true;
                    SerializationLocation = (attr as YmlValueForAttribute).Parent;
                }
            }
            else if(attr is YmlDontSerializeAttribute)
                IsAttributedAsDontSerialize = true;
            else if(attr is YmlSerializeAsAttribute)
                Alias = (attr as YmlSerializeAsAttribute).SerializeAs;
            else if(attr is YmlElementForAttribute)
            {
                IsSerializedAsElement = true;
                SerializationLocation = (attr as YmlElementForAttribute).Parent;
            }
            else if(attr is YmlCollectionAttribute)
                _collectionAttributeInstance = attr as YmlCollectionAttribute;
            else if(attr is YmlErrorIfMissedAttribute)
            {
                YmlErrorIfMissedAttribute temp = attr as YmlErrorIfMissedAttribute;
                DefaultValue = temp.DefaultValue;
                TreatErrorsAs = temp.TreatAs;
            }
            else if(attr is YmlFormatAttribute)
                Format = (attr as YmlFormatAttribute).Format;
            else if(attr is YmlNotCollectionAttribute)
            {
                // arrays are always treated as collections
                if(!ReflectionUtils.IsArray(MemberType))
                    IsAttributedAsNotCollection = true;
            }
            else if(attr is YmlCustomSerializerAttribute)
            {
                Type serType = (attr as YmlCustomSerializerAttribute).CustomSerializerType;

                Type genTypeArg;
                bool isDesiredInterface = ReflectionUtils.IsDerivedFromGenericInterfaceType(serType,
                                                                                            typeof(ICustomSerializer<>),
                                                                                            out genTypeArg);

                if(!isDesiredInterface)
                    throw new YmlException(
                        "The provided custom serialization type is not derived from the proper interface");
                if(genTypeArg != MemberType)
                    throw new YmlException("The generic argument of the class and the member type do not match");
                CustomSerializerType = serType;
            }
            else if(attr is YmlSerializableTypeAttribute)
                throw new Exception("This attribute is not applicable to fields and properties!");
        }

        #endregion Methods 
    }
}